﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Linq.Expressions;

namespace AnaxibiaStudios.Smug.Internal
{
  public interface IInvocationHandler
  {
    object Handle(object target, MethodInfo methodInfo, Func<object> baseCall, params object[] parameters);
  }

  public class InvocationHandler : IInvocationHandler
  {
    private static System.Threading.ThreadLocal<bool> _useRealForConstructorOverrideThreadStorage = new System.Threading.ThreadLocal<bool>();

    public static void SetRealCtorCallsOverride(bool useReals)
    {
      _useRealForConstructorOverrideThreadStorage.Value = useReals;
    }

    public static IInvocationHandler Default = new InvocationHandler();

    public virtual object Handle(object target, MethodInfo methodInfo, Func<object> baseCall, params object[] parameters)
    {
      if (_useRealForConstructorOverrideThreadStorage.Value)
      {
        return baseCall();
      }

      return null;
    }

    public static IInvocationHandler GetOrDefault(IInvocationHandler handler)
    {
      return handler ?? Default;
    }

    public static MethodInfo GetExplicitMethodInfo(MethodCallExpression callExpression, object target)
    {
      var baseType = target.GetType().BaseType;
      var knownMethodInfo = callExpression.Method;
      var builder = new SourceBuilder(baseType); // only types with base types will attempt this call

      Func<MethodInfo, bool, string> getSig = (minfo, useExplicit) => MemberInfoEx.SoloWrap(builder, minfo).BuildSignature(new SignatureOptions
      {
        IgnoreAccess = true,
        OverrideText = string.Empty,
        UseExplicit = useExplicit
      });

      string sig = getSig(knownMethodInfo, true);

      var baseMethods = baseType.GetMethods(Constants.Flags).Select(mi => getSig(mi, false)).ToArray();
      var match = baseType.GetMethods(Constants.Flags).FirstOrDefault(mi => getSig(mi, false) == sig);

      return match;
    }

    public static Func<T> GetExplicitMethod<T>(Expression<Func<T>> snatch, object target, object[] args)
    {
      var explicitInfo = GetExplicitMethodInfo((MethodCallExpression)snatch.Body, target);

      Func<T> invocation = () => (T)explicitInfo.Invoke(target, args);

      return invocation;
    }

    public static Action GetExplicitMethod(Expression<Action> snatch, object target, object[] args)
    {
      var explicitInfo = GetExplicitMethodInfo((MethodCallExpression)snatch.Body, target);

      Action invocation = () => explicitInfo.Invoke(target, args);

      return invocation;
    }

    public static Func<object> GetExplicitProperty(Type interfaceType, string propertyName, object target, object[] args, Type valueType, bool setter)
    {
      var interfaceProperty = interfaceType.GetProperty(propertyName, Constants.Flags, null, valueType, Type.GetTypeArray(args), null);
      var targetType = target.GetType();
      var baseType = targetType.BaseType;

      string interfaceFullName = SourceBuilder.TypeName(interfaceType);
      string noglobal = interfaceFullName.Substring("global::".Length);

      var paramList = new List<Type>();
      paramList.AddRange(Type.GetTypeArray(args));

      if (setter)
      {
        paramList.RemoveAt(paramList.Count - 1);
      }

      var baseProperty = baseType.GetProperty(noglobal + SourceBuilderBase.Dot + propertyName, Constants.Flags, null, valueType, paramList.ToArray(), null);

      var getAccess = setter ?
        ((Func<bool, MethodInfo>)baseProperty.GetSetMethod) :
        ((Func<bool, MethodInfo>)baseProperty.GetGetMethod);

      var access = getAccess(true) ?? getAccess(false);

      Func<object> invocation = () => access.Invoke(target, args);

      return invocation;
    }

    public static Action GetExplicitEventAccess(Type interfaceType, string eventName, object target, bool isAdd, Delegate listener)
    {
      var interfaceEvent = interfaceType.GetEvent(eventName, Constants.Flags);
      var targetType = target.GetType();
      var baseType = targetType.BaseType;

      string interfaceFullName = SourceBuilder.TypeName(interfaceType);
      string noglobal = interfaceFullName.Substring("global::".Length);

      var baseEvent = baseType.GetEvent(noglobal + SourceBuilderBase.Dot + eventName, Constants.Flags);
      var getAccess = isAdd ?
        ((Func<bool, MethodInfo>)baseEvent.GetAddMethod) :
        ((Func<bool, MethodInfo>)baseEvent.GetRemoveMethod);

      var access = getAccess(true) ?? getAccess(false);

      Action invocation = () => access.Invoke(target, new object[] { listener });
      return invocation;
    }
  }
}
